﻿using System.Collections.Generic;
using System.Linq;

namespace HazeCore.Persistence
{
    /// <summary>
    ///  A class that serialises an array of items of specified type as per the format 
    ///   1. Fixed-length header including
    ///      4-byte FourCC that describes the type of the file
    ///      4-byte code for version of the file
    ///      8-byte length of the file including header
    ///      8-byte number that represents the number of items combined with file length
    ///        can determine the offset of the starting point of the index list
    ///   2. Data section that consists of all the items in the order they present in the array
    ///   3. Immediately after an array of indices to the entries above 
    ///  Note: all numeric values are stored little-endian
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class TypeSequencer<T> : ITypeSequencer<T> where T : IBinaryPersistable, new()
    {
        #region Constants

        public const int HeaderSize = 32;
        public const uint CurrentVersion = 0x01000000;
        public const string FourCC = "TSEQ";
        private const int CopyBufSize = 4096;

        #endregion

        #region Delegates

        private delegate void OnDataMove(byte[] buffer, int len, long oldPos, long newPos);

        #endregion

        #region Properties

        public long Count { get; private set; }

        #endregion

        #region Constructors

        protected TypeSequencer(IBinaryPersister binaryPersister)
        {
            _binaryPersister = binaryPersister;
        }

        public static TypeSequencer<T> CreateNew(IBinaryPersister binaryPersister)
        {
            
            var ts = new TypeSequencer<T>(binaryPersister)
                         {
                             Count = 0,
                             _length = HeaderSize,
                             _version = 0x01000000,
                             _isFixedSized = new T().IsFixedSized
                         };
            FourCC.CopyTo(0, ts._fourcc, 0, FourCC.Length);

            ts.WriteHeader();

            return ts;
        }

        public static TypeSequencer<T> Open(IBinaryPersister binaryPersister)
        {
            var ts = new TypeSequencer<T>(binaryPersister);
            
            return ts.ReadHeader() ? ts : null;
        }

        #endregion

        #region Methods

        #region ITypeSequencer Members

        public void InsertRange(long index, IList<T> items)
        {
            var gap = 0L;
            var maxSize = 0;
            foreach (var item in items)
            {
                gap += item.Size;
                if (item.Size > maxSize)
                {
                    maxSize = item.Size;
                }
            }
            
            // moves to the point to insert the items at
            var leftPoint = MoveTo(index);

            long donePos;
            var lenInc = gap + items.Count*sizeof (long);
            long lenToMove;
            if (!_isFixedSized)
            {   // modifies accordingly the offsets to the items after the insertion point
                var firstOffsetLloc = GetIndexLocation(index);
                lenToMove = _length - firstOffsetLloc;
                MoveDataForward(firstOffsetLloc, firstOffsetLloc + lenInc, lenToMove, CopyBufSize, 
                                (buf, len, oldPos, newPos) =>
                                    {
                                        var numIndicesInBuf = len/sizeof (long);
                                        for (var i = 0; i < numIndicesInBuf; i++)
                                        {
                                            var offset = BytesToPositiveLongLittleEndian(buf, i*sizeof (long));
                                            offset += gap;
                                            ULongToBytesLittleEndian((ulong)offset, buf, i*sizeof (long));
                                        }
                                    });
                var bufInsertedItemsOffsets = new byte[items.Count*sizeof (long)];
                var insertedItemOffset = leftPoint;
                for (var i = 0; i < items.Count; i++)
                {
                    var item = items[i];
                    ULongToBytesLittleEndian((ulong)insertedItemOffset, bufInsertedItemsOffsets, i * sizeof(long));
                    insertedItemOffset += item.Size;
                }
                _binaryPersister.Position = firstOffsetLloc + gap;
                _binaryPersister.Write(bufInsertedItemsOffsets, 0, bufInsertedItemsOffsets.Length);

                donePos = firstOffsetLloc;
            }
            else
            {
                donePos = _length;
            }

            // shifts the chunk after the point across to make the gap for insertion
            lenToMove = donePos - leftPoint;
            MoveDataForward(leftPoint, leftPoint + gap, lenToMove, CopyBufSize, null);

            // fits the items to insert to the gap
            var pos = leftPoint;
            var itemBuffer = new byte[maxSize];
            foreach (var item in items)
            {
                _binaryPersister.Position = pos;
                item.SaveToBuffer(itemBuffer);
                _binaryPersister.Write(itemBuffer, 0, item.Size);
                pos += item.Size;
            }

            Count += items.Count;
            _length += lenInc;
            UpdateLengthAndCount();
        }

        public void RemoveRange(long index, long count)
        {
            var gap = 0L;

            var t = new T();
            for (var i = index; i < index + count; i++)
            {
                MoveTo(i);
                t.LoadFromPersister(_binaryPersister);
                gap += t.Size;
            }

            var endLoc = !_isFixedSized ? GetIndexLocation(index) : _length;
            MoveDataBackward(index, index - gap, endLoc - index, CopyBufSize, null);
            var lenDec = gap + count*sizeof (long);
            if (!_isFixedSized)
            {
                var moveStart = endLoc + count*sizeof (long);
                MoveDataForward(moveStart, moveStart - lenDec, _length - moveStart, CopyBufSize,
                                (buf, len, oldPos, newPos) =>
                                    {
                                        var numIndicesInBuf = len / sizeof(long);
                                        for (var i = 0; i < numIndicesInBuf; i++)
                                        {
                                            var offset = BytesToPositiveLongLittleEndian(buf, i * sizeof(long));
                                            offset -= gap;
                                            ULongToBytesLittleEndian((ulong)offset, buf, i * sizeof(long));
                                        }
                                    });
            }

            Count -= count;
            _length -= lenDec;
            UpdateLengthAndCount();
        }

        public void SwapRange(long index1, long count1, long index2, long count2)
        {
            System.Diagnostics.Trace.Assert(index1 >= 0 && index2 >= 0 && count1 > 0 && count2 > 0);
            if (index1 + count1 <= index2)
            {
                SwapRangeOrdered(index1, count1, index2, count2);
            }
            else if (index2 + count2 <= index1)
            {
                SwapRangeOrdered(index2, count2, index1, count1);
            }
        }

        public void ReplaceRange(long index, long count, IList<T> items)
        {
            var totalSizeToAdd = items.Aggregate(0L, (current, item) => current + item.Size);

            var totalSizeToRemove = 0L;
            var t = new T();
            var leftPoint = MoveTo(index);
            for (var i = index; i < index + count; i++)
            {
                MoveTo(i);
                t.LoadFromPersister(_binaryPersister);
                totalSizeToRemove += t.Size;
            }
            var rightPoint = MoveTo(index + count);
            var indexLoc = GetIndexLocation(index);
            var unaffectedLen = indexLoc - rightPoint;
            var maxSize = items.Select(item => item.Size).Concat(new[] { 0 }).Max();
            var itemBuffer = new byte[maxSize];
            var itemsOffsetsBuffer = new byte[items.Count * sizeof(long)];

            var diff = totalSizeToAdd - totalSizeToRemove;

            MoveData(rightPoint, rightPoint + diff, unaffectedLen, CopyBufSize, null);
            
            var pos = leftPoint;
            for (var i = 0; i < items.Count; i++)
            {
                var item = items[i];
                _binaryPersister.Position = pos;
                item.SaveToBuffer(itemBuffer);
                _binaryPersister.Write(itemBuffer, 0, item.Size);
                ULongToBytesLittleEndian((ulong)pos, itemsOffsetsBuffer, i * sizeof(long));
                pos += item.Size;
            }

            if (!_isFixedSized) return;

            var newIndexLoc = indexLoc + diff;
            _binaryPersister.Position = newIndexLoc;
            _binaryPersister.Write(itemsOffsetsBuffer, 0, itemsOffsetsBuffer.Length);

            var tailing = indexLoc + count * sizeof(long);
            var newTailing = newIndexLoc + items.Count * sizeof(long);

            MoveData(tailing, newTailing, _length - tailing, CopyBufSize,
                     (buf, len, oldPos, newPos) =>
                         {
                             var numIndicesInBuf = len / sizeof(long);
                             for (var i = 0; i < numIndicesInBuf; i++)
                             {
                                 var offset = BytesToPositiveLongLittleEndian(buf, i * sizeof(long));
                                 offset -= diff;
                                 ULongToBytesLittleEndian((ulong)offset, buf, i * sizeof(long));
                             }
                         });
        }

        public T Retrieve(long index)
        {
            MoveTo(index);
            var t = new T();
            t.LoadFromPersister(_binaryPersister);

            return t;
        }

        public IList<T> RetrieveRange(long start, int count)
        {
            var list = new List<T>();
            for (var i = start; i < start + count; i++)
            {
                list.Add(Retrieve(i));
            }
            return list;
        }

        #endregion

        private static uint BytesToUIntLittleEndian(IList<byte> buf, int pos)
        {
            uint result = 0;
            for (var i = pos; i < pos + 4; i++)
            {
                result <<= 8;
                result |= buf[i];
            }
            return result;
        }

        private static ulong BytesToULongLittleEndian(IList<byte> buf, int pos)
        {
            ulong result = 0;
            for (var i = pos; i < pos + 8; i++)
            {
                result <<= 8;
                result |= buf[i];
            }
            return result;
        }

        private static long BytesToPositiveLongLittleEndian(IList<byte> buf, int pos)
        {
            if ((buf[pos + 7] & 0x80) != 0) return -1;
            return (long)BytesToULongLittleEndian(buf, pos);
        }

        private static void UIntToBytesLittleEndian(uint value, IList<byte> buf, int pos)
        {
            for (var i = pos; i < pos + 4; i++)
            {
                buf[i] = (byte)(value & 0xff);
                value >>= 8;
            }
        }

        private static void ULongToBytesLittleEndian(ulong value, IList<byte> buf, int pos)
        {
            for (var i = pos; i < pos + 8; i++)
            {
                buf[i] = (byte) (value & 0xff);
                value >>= 8;
            }
        }

        private bool ReadHeader()
        {
            var buf = new byte[HeaderSize];
            _binaryPersister.Position = 0;
            var nRead = _binaryPersister.Read(buf, 0, buf.Length);
            if (nRead < buf.Length)
            {
                return false;
            }

            for (var i = 0; i < 4; i++)
            {
                _fourcc[i] = (char)buf[i];
            }

            _version = BytesToUIntLittleEndian(buf, 4);
            _length = BytesToPositiveLongLittleEndian(buf, 8);
            if (_length < 0) return false;
            Count = BytesToPositiveLongLittleEndian(buf, 16);
            if (Count < 0) return false;
            _typeId = BytesToUIntLittleEndian(buf, 24);

            var tempObj = new T();
            if (tempObj.TypeId != _typeId)
            {
                return false;
            }

            _isFixedSized = tempObj.IsFixedSized;
            if (_isFixedSized)
            {
                _fixedSize = tempObj.Size;
            }

            return true;
        }

        private void WriteHeader()
        {
            var buf = new byte[HeaderSize];

            for (var i = 0; i < 4; i++)
            {
                buf[i] = (byte)_fourcc[i];
            }

            UIntToBytesLittleEndian(_version, buf, 4);
            ULongToBytesLittleEndian((ulong)_length, buf, 8);
            ULongToBytesLittleEndian((ulong) Count, buf, 16);
            UIntToBytesLittleEndian(_typeId, buf, 24);

            _binaryPersister.Position = 0;
            _binaryPersister.Write(buf, 0, buf.Length);
        }

        private void UpdateLengthAndCount()
        {
            var buf = new byte[16];
            
            var temp64 = _length;
            for (var i = 0; i < 8; i++)
            {
                buf[i] = (byte)(temp64 & 0xff);
                temp64 >>= 8;
            }
            temp64 = Count;
            for (var i = 8; i < 16; i++)
            {
                buf[i] = (byte)(temp64 & 0xff);
                temp64 >>= 8;
            }

            _binaryPersister.Position = 8;
            _binaryPersister.Write(buf, 0, buf.Length);
            _binaryPersister.Length = _length;
        }

        private long MoveTo(long index)
        {
            return _isFixedSized ? MoveToFixedSized(index) : MoveToVariableSized(index);
        }

        private long MoveToFixedSized(long index)
        {
            var dataOffset = HeaderSize + index*_fixedSize;
            _binaryPersister.Position = dataOffset;
            return dataOffset;
        }

        private long MoveToVariableSized(long index)
        {
            var buf = new byte[8];
            _binaryPersister.Position = GetIndexLocation(index);
            _binaryPersister.Read(buf, 0, 8);
            if ((buf[8] & 0x80) != 0)
            {
                return -1;
            }
            long dataOffset = 0;
            foreach (var b in buf)
            {
                dataOffset <<= 8;
                dataOffset += b;
            }
            _binaryPersister.Position = dataOffset;
            return dataOffset;
        }

        private long GetIndexLocation(long index)
        {
            var indexOffset = _length;
            indexOffset -= (Count - index) * 8;
            return indexOffset;
        }

        private void MoveData(long oldPos, long newPos, long len, int bufSize, OnDataMove handleOnMove)
        {
            if (oldPos < newPos)
            {
                MoveDataForward(oldPos, newPos, len, bufSize, handleOnMove);
            }
            else
            {
                MoveDataBackward(oldPos, newPos, len, bufSize, handleOnMove);
            }
        }

        private void MoveDataBackward(long oldPos, long newPos, long len, int bufSize, OnDataMove handleOnMove)
        {
            System.Diagnostics.Trace.Assert(oldPos > newPos);

            var buf = new byte[bufSize];
            var gap = newPos - oldPos;
            var src = oldPos;
            var endPos = newPos + len;
            var stopPos = endPos - bufSize;

            int readLen;
            long dest;

            for (; src <= stopPos; src += bufSize)
            {
                _binaryPersister.Position = src;
                readLen = _binaryPersister.Read(buf, 0, bufSize);

                dest = src + gap;
                if (handleOnMove != null)
                {
                    handleOnMove(buf, readLen, src, dest);
                }

                _binaryPersister.Position = dest;
                _binaryPersister.Write(buf, 0, readLen);
            }

            if (src >= endPos) return;

            _binaryPersister.Position = src;
            var lenToRead = (int)(endPos - src);
            readLen = _binaryPersister.Read(buf, 0, lenToRead);
            dest = src + gap;
            if (handleOnMove != null)
            {
                handleOnMove(buf, readLen, src, dest);
            }

            _binaryPersister.Position = dest;
            _binaryPersister.Write(buf, 0, readLen);
        }

        private void MoveDataForward(long oldPos, long newPos, long len, int bufSize, OnDataMove handleOnMove)
        {
            System.Diagnostics.Trace.Assert(oldPos < newPos);

            var buf = new byte[bufSize];
            var gap = newPos - oldPos;
            var src = oldPos + len - bufSize;

            int readLen;
            long dest;

            for (; src >= oldPos; src -= bufSize)
            {
                _binaryPersister.Position = src;
                readLen = _binaryPersister.Read(buf, 0, bufSize);

                dest = src + gap;
                if (handleOnMove != null)
                {
                    handleOnMove(buf, readLen, src, dest);
                }

                _binaryPersister.Position = dest;
                _binaryPersister.Write(buf, 0, readLen);
            }

            if (src + bufSize <= oldPos) return;

            src = _binaryPersister.Position = oldPos;
            var lenToRead = (int) (oldPos - src);
            readLen = _binaryPersister.Read(buf, 0, lenToRead);
            dest = src + gap;
            if (handleOnMove != null)
            {
                handleOnMove(buf, readLen, src, dest);
            }
            _binaryPersister.Position = dest;
            _binaryPersister.Write(buf, 0, readLen);
        }

        private void SwapRangeOrdered(long index1, long count1, long index2, long count2)
        {
            var ofs1 = MoveTo(index1);
            var ofs1End = MoveTo(index1 + count1);
            var lenChunk1 = ofs1End - ofs1;
            var ofs2 = MoveTo(index2);
            var ofs2End = MoveTo(index2 + count2);
            var lenChunk2 = ofs2End - ofs2;
            var lenMid = ofs2 - ofs1End;

            if (lenChunk1 < lenChunk2)
            {
                MoveDataForward(ofs2, _length, lenChunk2, CopyBufSize, null);
                MoveDataForward(ofs1, ofs2End-lenChunk1, lenChunk1, CopyBufSize, null);
                if (lenMid > 0)
                {
                    MoveDataForward(ofs1End, ofs2End - lenChunk1, lenMid, CopyBufSize, null);
                }
                MoveDataBackward(_length, ofs1, lenChunk2, CopyBufSize, null);
            }
            else
            {
                MoveDataForward(ofs1, _length, lenChunk1, CopyBufSize, null);
                if (lenMid > 0)
                {
                    MoveDataBackward(ofs1End, ofs1, lenMid, CopyBufSize, null);
                }
                MoveDataBackward(ofs2, ofs1 + lenMid, lenChunk2, CopyBufSize, null);
                MoveDataBackward(_length, ofs1 + lenMid + lenChunk1, lenChunk1, CopyBufSize, null);
            }

            var ofsIndex1 = GetIndexLocation(index1);
            var ofsIndex2 = GetIndexLocation(index2);
            var countMid = index2 - index1 - count1;

            if (count1 < count2)
            {
                MoveDataForward(ofsIndex2, _length, count2*sizeof (long), CopyBufSize,
                                (buf, len, pos, newPos) =>
                                    {
                                        var numIndicesInBuf = len/sizeof (long);
                                        for (var i = 0; i < numIndicesInBuf; i++)
                                        {
                                            var offset = BytesToPositiveLongLittleEndian(buf, i*sizeof (long));
                                            offset += ofs1 - ofs2;
                                            ULongToBytesLittleEndian((ulong) offset, buf, i*sizeof (long));
                                        }
                                    });
                MoveDataForward(ofsIndex1, ofsIndex2 + (count2 - count1)*sizeof(long), count1*sizeof (long), CopyBufSize,
                                (buf, len, pos, newPos) =>
                                    {
                                        var numIndicesInBuf = len / sizeof(long);
                                        for (var i = 0; i < numIndicesInBuf; i++)
                                        {
                                            var offset = BytesToPositiveLongLittleEndian(buf, i * sizeof(long));
                                            offset += ofs2End - lenChunk1;
                                            ULongToBytesLittleEndian((ulong)offset, buf, i * sizeof(long));
                                        }
                                    });
                if (countMid > 0)
                {
                    MoveDataForward(ofsIndex1 + count1*sizeof (long), ofsIndex1 + count2*sizeof (long),
                                    countMid*sizeof (long),
                                    CopyBufSize,
                                    (buf, len, pos, newPos) =>
                                        {
                                            var numIndicesInBuf = len/sizeof (long);
                                            for (var i = 0; i < numIndicesInBuf; i++)
                                            {
                                                var offset = BytesToPositiveLongLittleEndian(buf, i*sizeof (long));
                                                offset += lenChunk2 - lenChunk1;
                                                ULongToBytesLittleEndian((ulong) offset, buf, i*sizeof (long));
                                            }
                                        });
                }
                MoveDataBackward(_length, ofsIndex1, count2*sizeof(long), CopyBufSize, null);
            }
            else
            {
                MoveDataForward(ofsIndex1, _length, count1*sizeof (long), CopyBufSize,
                                (buf, len, pos, newPos) =>
                                    {
                                        var numIndicesInBuf = len / sizeof(long);
                                        for (var i = 0; i < numIndicesInBuf; i++)
                                        {
                                            var offset = BytesToPositiveLongLittleEndian(buf, i * sizeof(long));
                                            offset += ofs2End - lenChunk1;
                                            ULongToBytesLittleEndian((ulong)offset, buf, i * sizeof(long));
                                        }
                                    });
                if (countMid > 0)
                {
                    MoveDataBackward(ofsIndex1 + count1*sizeof (long), ofsIndex1 + count2*sizeof (long),
                                     countMid*sizeof (long),
                                     CopyBufSize,
                                     (buf, len, pos, newPos) =>
                                         {
                                             var numIndicesInBuf = len/sizeof (long);
                                             for (var i = 0; i < numIndicesInBuf; i++)
                                             {
                                                 var offset = BytesToPositiveLongLittleEndian(buf, i*sizeof (long));
                                                 offset += lenChunk2 - lenChunk1;
                                                 ULongToBytesLittleEndian((ulong) offset, buf, i*sizeof (long));
                                             }
                                         });
                }
                MoveDataBackward(ofsIndex2, ofsIndex1, count2*sizeof (long), CopyBufSize,
                                 (buf, len, pos, newPos) =>
                                     {
                                         var numIndicesInBuf = len/sizeof (long);
                                         for (var i = 0; i < numIndicesInBuf; i++)
                                         {
                                             var offset = BytesToPositiveLongLittleEndian(buf, i*sizeof (long));
                                             offset += ofs1 - ofs2;
                                             ULongToBytesLittleEndian((ulong) offset, buf, i*sizeof (long));
                                         }
                                     });
                MoveDataBackward(_length, ofsIndex2 + (count2 - count1)*sizeof (long), count1*sizeof (long), CopyBufSize,
                                 null);
            }

            UpdateLengthAndCount();
        }

        #endregion

        #region Fields

        private readonly IBinaryPersister _binaryPersister;

        private readonly char[] _fourcc = new char[4];
        private uint _version;
        private long _length;
        private uint _typeId;
        private bool _isFixedSized;
        private int _fixedSize;

        #endregion
    }
}
